﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Windows.Storage;
using WinRTDatabase.Core;
using WinRTDatabase.Exceptions;

namespace WinRTDatabase
{
    /// <summary>
    /// Represents a table that contains rows of type T
    /// </summary>
    /// <typeparam name="T">Row type</typeparam>
    public class Table<T> : ObservableCollection<T>, ITable, INotifyPropertyChanged
        where T : class, INotifyPropertyChanged
    {
        private Database _database;
        private bool _raiseCollectionChangedEvents = true;
        private string _tableName = string.Empty;
        private bool _isDirty = false;

        /// <summary>
        /// Constructor for table - use Database.CreateTable instead
        /// </summary>
        [Obsolete("Do not use constructor - use Database.CreateTable() instead")]
        public Table() { }

        /// <summary>
        /// Only used internally to load items into Table object
        /// </summary>
        /// <param name="collection">Collection of items to load</param>
        [Obsolete("Do not use constructor -internal use only")]
        public Table(ObservableCollection<T> collection)
        {
            AddRange(collection);
        }

        /// <summary>
        /// New instance of a table class
        /// </summary>
        /// <param name="database">Database name that table belongs to</param>
        /// <param name="tableName">If a table is named, contains table name, otherwise empty string</param>
        private Table(Database database, string tableName)
        {
            _database = database;
            _tableName = tableName;
        }

        /// <summary>
        /// Set internal variables data when a table is de-serialized
        /// </summary>
        /// <param name="database">Database name that table belongs to</param>
        /// <param name="tableName">If a table is named, contains table name, otherwise empty string</param>
        /// <param name="markClean">If set to true, table will be mark as clean</param>
        public void SetTableDefinition(Database database, string tableName = "", bool markClean = true)
        {
            _database = database;
            _tableName = tableName;
            MarkClean();
            foreach (T row in this)
            {
                row.PropertyChanged += RowPropertyChanged;
            }
            MarkClean();
        }

        void RowPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            MarkDirty();
        }


        /// <summary>
        /// If a table is named, contains table name, otherwise empty string
        /// </summary>
        public string TableName { get { return _tableName; } }

        /// <summary>
        /// Create new table for specified database
        /// </summary>
        /// <param name="database">Database name that table belongs to</param>
        /// <param name="tableName">If a table is named, contains table name, otherwise empty string</param>
        /// <returns>Instance of created table</returns>
        internal static Table<T> CreateTable(Database database, string tableName = "")
        {
            return new Table<T>(database, tableName);
        }


        /// <summary>
        /// Save table to isolated storage
        /// </summary>
        /// <exception cref="WinRTDatabase.Exceptions.SaveException">Thrown when an error occurs during save</exception>
        async public Task<SaveResult> SaveAsync()
        {

            if (_database != null)
            {
                try
                {
                    _database.IncrementBusy();
                    var folder = ApplicationData.Current.LocalFolder;
                    if (_database.StorageLocation == StorageLocation.Roaming)
                    {
                        folder = ApplicationData.Current.RoamingFolder;
                    }
                    var fileName = Utility.GetFileName(_database.DatabaseName, typeof(T), _tableName);
                    string content = await GetTableContent();

                    var file = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);

                    using (var stream = await file.OpenStreamForWriteAsync())
                    {
                        var bytes = Encoding.UTF8.GetBytes(content);
                        await stream.WriteAsync(bytes, 0, bytes.Length);
                        await stream.FlushAsync();
                    }

                    return new SaveResult(null);
                }
                catch (Exception ex)
                {
                    throw new SaveException(ex);
                }
                finally
                {
                    _database.DecrementBusy();
                    MarkClean();
                }
            }
            else
            {
                throw new TableCannotBeSavedException(ResourceHelper.GetResourceString("TableWithoutDatabaseCannotBeSaved"));
            }
        }


        /// <summary>
        /// Get content of a table
        /// </summary>
        async public Task<string> GetTableContent()
        {
            string content;
            var stringWriter = new StringWriter();
            var serializer = new XmlSerializer(typeof(ObservableCollection<T>));

            await Task.Run(() => { serializer.Serialize(stringWriter, this); });
            await Task.Run(() => { stringWriter.Flush(); });
            content = stringWriter.GetStringBuilder().ToString();

            return content;
        }

        /// <summary>
        /// Raises collection changed event
        /// </summary>
        /// <param name="e">Event arguments for collection changed event</param>
        protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (_raiseCollectionChangedEvents)
            {
                base.OnCollectionChanged(e);
                if (e.OldItems != null)
                {
                    foreach (T item in e.OldItems)
                    {
                        item.PropertyChanged -= RowPropertyChanged;
                    }
                }
                if (e.NewItems != null)
                {
                    foreach (T item in e.NewItems)
                    {
                        item.PropertyChanged += RowPropertyChanged;
                    }
                }
                MarkDirty();
            }
        }

        /// <summary>
        /// Add a range of items to the table
        /// </summary>
        /// <param name="range">Range of items</param>
        public void AddRange(IEnumerable<T> range)
        {
            _raiseCollectionChangedEvents = false;
            try
            {
                foreach (var item in range)
                {
                    Add(item);
                    item.PropertyChanged += RowPropertyChanged;
                }
            }
            finally
            {
                _raiseCollectionChangedEvents = true;
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
            }
        }

        /// <summary>
        /// Remove a range of items from the table
        /// </summary>
        /// <param name="range">Range of items to remove</param>
        public void RemoveRange(IEnumerable<T> range)
        {
            _raiseCollectionChangedEvents = false;
            try
            {
                foreach (var item in range)
                {
                    Remove(item);
                }
            }
            finally
            {
                _raiseCollectionChangedEvents = true;
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
            }
        }

        /// <summary>
        /// Delete a range of items from the table based on
        /// condition being met
        /// </summary>
        /// <param name="deleteCondition">
        /// Condition that must be met to delete the items
        /// </param>
        public void RemoveRange(Func<T, bool> deleteCondition)
        {
            var deletedItems = (from oneItem in this.Items
                                where deleteCondition(oneItem)
                                select oneItem);
            // ReSharper disable PossibleMultipleEnumeration
            if (deletedItems.Count() > 0)
            // ReSharper restore PossibleMultipleEnumeration
            {
                // ReSharper disable PossibleMultipleEnumeration
                RemoveRange(deletedItems.ToArray());
                // ReSharper restore PossibleMultipleEnumeration
            }
        }


        #region ITable Members

        /// <summary>
        /// Type of object that this table contains
        /// </summary>
        public Type RowType
        {
            get { return typeof(T); }
        }


        /// <summary>
        /// Returns true of any data in the table has been changed by the user.
        /// This includes changes to rows, addition or removal of rows.
        /// </summary>
        public bool IsDirty
        {
            get { return _isDirty; }
        }

        /// <summary>
        /// Mark table as dirty
        /// </summary>
        protected internal void MarkDirty()
        {
            _isDirty = true;
            RaisePropertyChanged("IsDirty");
            if (_database != null)
            {
                _database.RaiseDirty();
            }
        }

        /// <summary>
        /// Mark table as clean
        /// </summary>
        protected internal void MarkClean()
        {
            _isDirty = false;
            RaisePropertyChanged("IsDirty");
            _database.RaiseDirty();
        }

        #endregion

        /// <summary>
        /// Raise PropertyChanged event
        /// </summary>
        /// <param name="propertyName"></param>
        protected void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        /// <summary>
        /// Event indicatiting that a property value changed in this instance of the class
        /// </summary>
        public new event PropertyChangedEventHandler PropertyChanged;
    }
}
